/*******************************************************************************
 * Copyright (c) 2011 TXT e-solutions SpA
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * This work was performed within the IoT_at_Work Project
 * and partially funded by the European Commission's
 * 7th Framework Programme under the research area ICT-2009.1.3
 * Internet of Things and enterprise environments.
 *
 *
 * Authors:
 *     Cristoforo Seccia (TXT e-solutions SpA)
 *
 * Contributors:
 *      Domenico Rotondi (TXT e-solutions SpA)
 *******************************************************************************/
package it.txt.access.capability.demo.client.tcp;

import it.txt.access.capability.commons.utils.ClientServerUtils;
import it.txt.access.capability.demo.client.view.model.ServerConnectionParamsModel;
import it.txt.access.capability.demo.client.view.model.ServerResponseInfoModel;
import java.beans.PropertyChangeListener;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.Socket;
import org.w3c.dom.Element;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.Document;

/**
 *
 * @author Cristoforo Seccia (TXT e-solutions SpA)
 */
public class RequestClient implements Runnable {

    private static final Logger LOGGER = Logger.getLogger(RequestClient.class.getName());
    private RequestClientSupport progressSupport;
    private Element signedRequestElement = null;
    private boolean isConnected = false;
    private Socket clientSocket = null;
    private DataOutputStream outChannel;
    private DataInputStream inChannel;

    public RequestClient(ServerConnectionParamsModel model, Document signedRequest) throws IOException{
        openClientSocket(model.getServerAddressIP(), model.getServerTCPPort());
        progressSupport = new RequestClientSupport();
        signedRequestElement = signedRequest.getDocumentElement();
    }

    public RequestClient(ServerConnectionParamsModel model, Document signedRequest, PropertyChangeListener listener) 
            throws IOException{
        this(model, signedRequest);
        progressSupport.addPropertyChangeListener(listener);
    }

    public synchronized boolean isConnected() {
        return isConnected;
    }

    public void closeClientSocket() {
        try {
            isConnected = false;
            //Close the client clientSocket.
            if (clientSocket != null) {
                LOGGER.log(Level.INFO, "Closing socket...");
                inChannel.close();
                outChannel.close();
                clientSocket.close();
            }
            LOGGER.log(Level.INFO, "Socket closed.");
        } catch (IOException ex) {
            LOGGER.log(Level.SEVERE, ex.getMessage(), ex);
        }
    }

    public void run() {
        //Check server connection enstablished
        if (!isConnected()) {
            //Fire new connection enstablished error event.
            fireConnectionWithServerErrorEvent();
            //close the client connection
            closeClientSocket();
            //Fire new service request process finished
            fireServiceRequestFinished(RequestClientSupport.STATE_ERROR);
            return;
        }
        //Fire new connection enstablished success event.
        fireConnectionWithServerSuccessEvent();
        //Init the server capability request into the soket out channel
        XMLOutputStream xmlos = initClientRequest(signedRequestElement);
        if (xmlos.getDataToSendLenght() == XMLOutputStream.EMPTY_LENGHT) {
            //Fire new service request ready error event.
            fireRequestReadyErrorEvent();
            //close the client connection
            closeClientSocket();
            //Fire new service request process finished
            fireServiceRequestFinished(RequestClientSupport.STATE_ERROR);
            return;
        }
        //Fire new service request ready success event.
        fireRequestReadySuccessEvent();
        //Send the client reques
        sendClientRequest(xmlos);
        if (xmlos.getDataSentLenght() == XMLOutputStream.INVALID_LENGHT) {
            //Fire new service request sent errro event.
            fireSendingRequestErrorEvent();
            //Close the clientSocket.
            closeClientSocket();
            //Fire new service request process finished
            fireServiceRequestFinished(RequestClientSupport.STATE_ERROR);
            return;
        }
        //Fire new service request sent success event.
        fireSendingRequestSuccessEvent();
        //The response may be a capability response (xml) or a plain message
        //returned if and only if the server occurred an internal error.
        XMLInputStream xmlis = receiveServerXMLResponse();
        if (xmlis.getDataReceivedLenght() == XMLInputStream.INVALID_LENGHT) {
            //Fire new response received error message.
            fireReceivingResponseErrorEvent();
            //Close the client clientSocket.
            closeClientSocket();
            //Fire new service request process finished
            fireServiceRequestFinished(RequestClientSupport.STATE_ERROR);
        }
        //recover the received response data.
        final byte[] receivedData = xmlis.getReceivedData();
        //check eceived response.
        ServerResponseInfoModel infoModel = checkServerXMLresponse(receivedData);
        if (infoModel == null) {
            //Fire new response received error message.
            fireReceivingResponseErrorEvent();
            //Trying to recover the plain error response.
            String plainResponse = receiveServerPlainResponse(receivedData);
            infoModel = new ServerResponseInfoModel();
            infoModel.setResponseID(ClientServerUtils.INVALID_RESPONSE_ID);
            infoModel.setServiceResponseStatus(plainResponse);
            //Fire the response  information
            fireReceivingResponseInfo(infoModel);
            //close the client connection
            closeClientSocket();
            //Fire new service request process finished
            fireServiceRequestFinished(RequestClientSupport.STATE_ERROR);
        } else {
            //Fire new response received success message
            fireReceivingResponseSuccessEvent();
            //Fire the response  information
            fireReceivingResponseInfo(infoModel);
            //close the client connection
            closeClientSocket();
            //Fire new service request process finished
            fireServiceRequestFinished(RequestClientSupport.STATE_OK);
        }
    }

    private void openClientSocket(String host, int port) throws IOException {
        //Initializing the clientSocket and in/out channels.
        LOGGER.log(Level.INFO, "Connecting to the Server using: {0} at: {1}", new Object[]{host, port});
        clientSocket = new Socket(host, port);
        inChannel = new DataInputStream(clientSocket.getInputStream());
        outChannel = new DataOutputStream(clientSocket.getOutputStream());
        LOGGER.log(Level.INFO, "Receiving connection response...");
        int len = inChannel.readInt();
        byte[] data = new byte[len];
        inChannel.read(data, 0, len);
        String connectionResp = new String(data);
        LOGGER.log(Level.INFO, "Connection response received: {0}", connectionResp);
        //Check the connection enstablished response.
        if (ClientServerUtils.RESPONSE_CONNECTION_SUCCESS.equals(connectionResp)) {
            LOGGER.log(Level.INFO, "Successfully connected to the server.");
            //Correctly enstablished connection
            isConnected = true;
        }
        else {
            LOGGER.log(Level.INFO, "Unoknow connection response received.");
            //Incorrectly enstablished connection
            isConnected = false;
        }
    }

    private XMLOutputStream initClientRequest(Element signedRequest) {
        XMLOutputStream xmlos = new XMLOutputStream();
        try {
            DOMSource source = new DOMSource(signedRequest);
            StreamResult result = new StreamResult(xmlos);
            TransformerFactory transFactory = TransformerFactory.newInstance();
            Transformer transformer = transFactory.newTransformer();
            transformer.transform(source, result);
            LOGGER.log(Level.INFO, "Writted data lenght: {0}", xmlos.getDataToSendLenght());
        } catch (TransformerException ex) {
            LOGGER.log(Level.SEVERE, ex.getMessage(), ex);
        }
        return xmlos;
    }

    private void sendClientRequest(XMLOutputStream xmlos) {
        try {
            //Send the capability request
            LOGGER.log(Level.INFO, "Sending Signed XML request...");
            xmlos.send(outChannel);
            LOGGER.log(Level.INFO, "Signed XML request sent.");
        } catch (IOException ex) {
            LOGGER.log(Level.SEVERE, ex.getMessage(), ex);
        }
    }

    private XMLInputStream receiveServerXMLResponse() {
        XMLInputStream xmlis = new XMLInputStream(inChannel);
        try {
            LOGGER.log(Level.INFO, "Receiving Server response...");
            xmlis.recive();
        } catch (IOException ex) {
            LOGGER.log(Level.SEVERE, ex.getMessage(), ex);
        }
        return xmlis;
    }

    private String receiveServerPlainResponse(final byte[] receivedData) {
        LOGGER.log(Level.INFO, "The received response is not an xml message..."
                + "check if the response contains a plain response");
        String response = new String(receivedData);
        LOGGER.log(Level.INFO, "Received plain response: {0}", response);
        return response;
    }

    private ServerResponseInfoModel checkServerXMLresponse(final byte[] receivedData) {
        //Recover the response...
        LOGGER.log(Level.INFO, "Server response received.");
        ServerResponseInfoModel infoModel = null;
        if (receivedData != null) {
            try {
                //recover the response information
                ByteArrayInputStream inputStream = new ByteArrayInputStream(receivedData);
                infoModel = RequestClientHelper.recoverServiceResponseInfo(inputStream);
                LOGGER.log(Level.INFO, "Server Response - ID: {0}", infoModel.getResponseID());
            } catch (ResponseClientException ex) {
                LOGGER.log(Level.SEVERE, ex.getMessage(), ex);
            }
        }
        return infoModel;
    }

    private void fireConnectionWithServerSuccessEvent() {
        //Fire new event connection enstablished ok.
        progressSupport.fireCapabilityServiceEvent(
                RequestClientSupport.EVENT_NAME_TCP_CONNECTION,
                RequestClientSupport.STATE_SETTINGS,
                RequestClientSupport.STATE_OK);
    }

    private void fireConnectionWithServerErrorEvent() {
        //Fire new event connection enstablished error.
        progressSupport.fireCapabilityServiceEvent(
                RequestClientSupport.EVENT_NAME_TCP_CONNECTION,
                RequestClientSupport.STATE_UNKNOW,
                RequestClientSupport.STATE_ERROR);
    }

    private void fireRequestReadySuccessEvent() {
        //Fire new event connection enstablished ok.
        progressSupport.fireCapabilityServiceEvent(
                RequestClientSupport.EVENT_NAME_SERVICE_REQUEST_READY,
                RequestClientSupport.STATE_SETTINGS,
                RequestClientSupport.STATE_OK);
    }

    private void fireRequestReadyErrorEvent() {
        //Fire new event connection enstablished ok.
        progressSupport.fireCapabilityServiceEvent(
                RequestClientSupport.EVENT_NAME_SERVICE_REQUEST_READY,
                RequestClientSupport.STATE_SETTINGS,
                RequestClientSupport.STATE_ERROR);
    }

    private void fireSendingRequestSuccessEvent() {
        //Fire new event service request received ok.
        progressSupport.fireCapabilityServiceEvent(
                RequestClientSupport.EVENT_NAME_SERVICE_REQUEST_SENT,
                RequestClientSupport.STATE_SETTINGS,
                RequestClientSupport.STATE_OK);
    }

    private void fireSendingRequestErrorEvent() {
        //Fire new event service request received error.
        progressSupport.fireCapabilityServiceEvent(
                RequestClientSupport.EVENT_NAME_SERVICE_REQUEST_SENT,
                RequestClientSupport.STATE_SETTINGS,
                RequestClientSupport.STATE_ERROR);
    }

    private void fireReceivingResponseSuccessEvent() {
        //Fire new event service connectionResp sent ok.
        progressSupport.fireCapabilityServiceEvent(
                RequestClientSupport.EVENT_NAME_SERVICE_RESPONSE_RECEIVED,
                RequestClientSupport.STATE_SETTINGS,
                RequestClientSupport.STATE_OK);
    }

    private void fireReceivingResponseErrorEvent() {
        //Fire new event service connectionResp sent error.
        progressSupport.fireCapabilityServiceEvent(
                RequestClientSupport.EVENT_NAME_SERVICE_RESPONSE_RECEIVED,
                RequestClientSupport.STATE_SETTINGS,
                RequestClientSupport.STATE_ERROR);
    }

    private void fireReceivingResponseInfo(ServerResponseInfoModel infoModel) {
        //Fire new event service connectionResp sent error.
        progressSupport.fireCapabilityResponseInfo(infoModel);
    }

    private void fireServiceRequestFinished(int newState) {
        //Fire new event service connectionResp sent error.
        progressSupport.fireCapabilityServiceEvent(
                RequestClientSupport.EVENT_NAME_SERVICE_REQUEST_FINISHED,
                RequestClientSupport.STATE_UNKNOW, newState);
    }

    public class XMLInputStream extends ByteArrayInputStream {

        private byte[] receivedData;
        private DataInputStream inChannel;
        public static final int INVALID_LENGHT = -1;
        private int receivedDataLenght = INVALID_LENGHT;

        public XMLInputStream(DataInputStream inChannel) {
            super(new byte[0]);
            this.inChannel = new DataInputStream(inChannel);
        }

        public int getDataReceivedLenght() {
            return receivedDataLenght;
        }

        public byte[] getReceivedData() {
            return receivedData;
        }

        public void recive() throws IOException {
            this.receivedDataLenght = INVALID_LENGHT;
            this.receivedDataLenght = this.inChannel.readInt();
            LOGGER.log(Level.INFO, "Received {0} of data from the client socket.", receivedDataLenght);
            this.receivedData = new byte[receivedDataLenght];
            this.inChannel.read(receivedData, 0, receivedDataLenght);
            this.buf = receivedData;
            this.count = receivedDataLenght;
            this.mark = 0;
            this.pos = 0;
        }
    }

    public class XMLOutputStream extends ByteArrayOutputStream {

        private byte[] dataSent;
        public static final int EMPTY_LENGHT = 0;
        public static final int INVALID_LENGHT = -1;
        private int dataSentLenght = INVALID_LENGHT;

        public int getDataToSendLenght() {
            return this.count;
        }

        public int getDataSentLenght() {
            return dataSentLenght;
        }

        public byte[] getDataSent() {
            return dataSent;
        }

        public void send(DataOutputStream outChannel) throws IOException {
            dataSentLenght = INVALID_LENGHT;
            this.dataSent = toByteArray();
            LOGGER.log(Level.INFO, "Sending {0} of data to the socket.", this.dataSent.length);
            outChannel.writeInt(this.dataSent.length);
            outChannel.write(this.dataSent);
            outChannel.flush();
            dataSentLenght = this.dataSent.length;
            reset();
        }
    }
}
